package dao

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"time"

	"open/pkg/bytedance"

	"github.com/go-kratos/kratos/v2/errors"
	"github.com/go-redis/redis/v8"
	"github.com/qiniu/go-sdk/v7/auth/qbox"
	"github.com/qiniu/go-sdk/v7/storage"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"

	admin "open/api/open/admin/v1"
	v1 "open/api/open/admin/v1"
	"open/internal/model"
	"open/internal/types"
)

const bytedanceAuthorizerAccessTokenKey = "open/bytedance/authorizer_access_token/%v" // auth_app_id
const bytedanceComponentAccessTokenKey = "open/bytedance/component_access_token/%v"   // app_id
const bytedanceUserSessionKey = "open/bytedance/session_key/%s/%s"

// GetBytedanceSession 获取用户session
func (d *Dao) GetBytedanceSession(ctx context.Context, authAppID, code, anonymousCode string) (session *bytedance.Session, err error) {
	var componentAppID, authorizerAccessToken string
	if componentAppID, authorizerAccessToken, err = d.GetBytedanceAuthorizerAccessToken(ctx, authAppID); err != nil {
		return
	}
	session, _, err = d.bytedance.MicroApp.Code2Session(ctx, componentAppID, authorizerAccessToken, code, anonymousCode)
	if err != nil {
		return nil, err
	}
	// 缓存session_key 用以解密用户加密数据
	if err := d.cache.Set(ctx, fmt.Sprintf(bytedanceUserSessionKey, authAppID, session.OpenID), session.SessionKey, time.Hour*24).Err(); err != nil {
		return nil, err
	}
	return
}

// GetBytedanceUserSessionKey 获取字节跳动登录的sessionKey
func (d *Dao) GetBytedanceUserSessionKey(ctx context.Context, appID, openID string) (string, error) {
	sessionKey, err := d.cache.Get(ctx, fmt.Sprintf(bytedanceUserSessionKey, appID, openID)).Result()
	if err != nil {
		if err == redis.Nil {
			return "", nil
		}
		return "", err
	}
	return sessionKey, nil
}

// SaveByteDanceOpenApp 保存字节跳动开放应用
func (d *Dao) SaveByteDanceOpenApp(ctx context.Context, app *model.BytedanceOpenApp) error {
	return d.FromContext(ctx).Save(app).Error
}

// CreateBytedanceSubApp 创建字节跳动小程序子应用
func (d *Dao) CreateBytedanceSubApp(ctx context.Context, app *model.BytedanceOpenSubApp) error {
	return d.FromContext(ctx).Create(app).Error
}

// SaveBytedanceSubApp 保存字节跳动小程序子应用
func (d *Dao) SaveBytedanceSubApp(ctx context.Context, app *model.BytedanceOpenSubApp) error {
	err := d.FromContext(ctx).Save(app).Error
	if err != nil {
		return err
	}
	return d.cache.Del(ctx, fmt.Sprintf(bytedanceAuthorizerAccessTokenKey, app.AppID)).Err()
}

// LockBytedanceAuthApp 获取字节跳动auth_app
func (d *Dao) LockBytedanceAuthApp(ctx context.Context, appID string) (*model.BytedanceOpenSubApp, error) {
	app := new(model.BytedanceOpenSubApp)
	err := d.FromContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("app_id = ?", appID).First(app).Error
	return app, err
}

// GetBytedanceAuthorizerAccessToken 获取授权商户调用凭证
func (d *Dao) GetBytedanceAuthorizerAccessToken(ctx context.Context, authAppID string) (componentAppID, authorizerAccessToken string, err error) {
	key := fmt.Sprintf(bytedanceAuthorizerAccessTokenKey, authAppID)
	authorizerAccessToken, err = d.cache.Get(ctx, key).Result()
	if err != nil {
		if err == redis.Nil {
			var authApp *model.BytedanceOpenSubApp
			if authApp, err = d.LockBytedanceAuthApp(ctx, authAppID); err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					err = errors.BadRequest(
						v1.ErrorCode_AUTH_UNAUTHORIZED.String(),
						"应用未授权或者商家主动取消授权,请重新授权后重试！")
				}
				return "", "", err
			}
			// 如果大于十分钟才过期, redis 清空出现
			if time.Until(authApp.AccessTokenExpiresAt) >= time.Minute*10 {
				d.cache.Set(ctx, key, authApp.AccessToken, time.Until(authApp.AccessTokenExpiresAt))
				return authApp.TpAppID, authApp.AccessToken, nil
			}
			var needRetrieve bool
			// 如果本地找不到有效token，调用字节跳动远端获取
			var componentAccessToken string
			if componentAppID, componentAccessToken, err = d.GetBytedanceComponentAccessToken(ctx); err != nil {
				if bErr, ok := err.(*bytedance.ErrorResponse); ok && bErr.ErrNo == 40021 {
					needRetrieve = true
				} else {
					return "", "", err
				}
			}
			var authorizerRefreshToken string
			var expiresIn int64
			// 如果刷新token过期，补偿获取
			if authApp.RefreshTokenExpiresAt.Before(time.Now()) || needRetrieve {
				var rr *bytedance.RetrieveAuthorizationCodeResponse
				rr, _, err = d.bytedance.ThirdParty.RetrieveAuthorizationCode(ctx, componentAppID, componentAccessToken, authAppID)
				if err != nil {
					return "", "", err
				}
				var oauth *bytedance.OAuthToken
				oauth, _, err = d.bytedance.ThirdParty.GetOAuthToken(ctx,
					componentAppID,
					componentAccessToken,
					rr.AuthorizationCode,
					"app_to_tp_authorization_code")
				if err != nil {
					return "", "", err
				}
				authorizerAccessToken = oauth.AuthorizerAccessToken
				authorizerRefreshToken = oauth.AuthorizerRefreshToken
				expiresIn = oauth.ExpiresIn

			} else {
				var rt *bytedance.RefreshOAuthTokenResponse
				if rt, _, err = d.bytedance.ThirdParty.RefreshOAuthToken(ctx,
					componentAppID,
					componentAccessToken,
					authApp.RefreshToken,
					"app_to_tp_refresh_token",
				); err != nil {
					if bErr, ok := err.(*bytedance.ErrorResponse); ok && bErr.ErrNo == 40021 {
						needRetrieve = true
					} else {
						return "", "", err
					}
				}
				if needRetrieve {
					var rr *bytedance.RetrieveAuthorizationCodeResponse
					rr, _, err = d.bytedance.ThirdParty.RetrieveAuthorizationCode(ctx, componentAppID, componentAccessToken, authAppID)
					if err != nil {
						return
					}
					var oauth *bytedance.OAuthToken
					oauth, _, err = d.bytedance.ThirdParty.GetOAuthToken(ctx,
						componentAppID,
						componentAccessToken,
						rr.AuthorizationCode,
						"app_to_tp_authorization_code")
					if err != nil {
						return "", "", err
					}
					authorizerAccessToken = oauth.AuthorizerAccessToken
					authorizerRefreshToken = oauth.AuthorizerRefreshToken
					expiresIn = oauth.ExpiresIn
				}
				if rt != nil {
					authorizerAccessToken = rt.AuthorizerAccessToken
					authorizerRefreshToken = rt.AuthorizerRefreshToken
					expiresIn = rt.ExpiresIn
				}
			}
			d.cache.Set(ctx, key, authorizerAccessToken, time.Duration(expiresIn-100)*time.Second)
			now := time.Now()
			authApp.AccessToken = authorizerAccessToken
			authApp.RefreshToken = authorizerRefreshToken
			authApp.AccessTokenExpiresAt = now.Add(time.Duration(expiresIn-100) * time.Second)
			authApp.RefreshTokenExpiresAt = now.Add(time.Hour*24*30 - 100*time.Second)
			if err = d.FromContext(ctx).Save(authApp).Error; err != nil {
				return "", "", err
			}
			return d.conf.Open.Bytedance.AppId, authorizerAccessToken, nil
		} else {
			return "", "", err
		}
	}
	return d.conf.Open.Bytedance.AppId, authorizerAccessToken, nil
}

// GetBytedanceComponentAccessToken 获取服务商调用凭证
func (d *Dao) GetBytedanceComponentAccessToken(ctx context.Context) (componentAppID, componentAccessToken string, err error) {
	key := fmt.Sprintf(bytedanceComponentAccessTokenKey, d.conf.Open.Bytedance.AppId)
	if componentAccessToken, err = d.cache.Get(ctx, key).Result(); err != nil && !errors.Is(err, redis.Nil) {
		return
	}
	if componentAccessToken == "" {
		var app *model.BytedanceOpenApp
		app, err = d.GetBytedanceOpenApp(ctx)
		if err != nil {
			return
		}
		var ct *bytedance.ComponentAccessToken
		ct, _, err = d.bytedance.ThirdParty.GetComponentAccessToken(ctx, app.AppID, app.AppSecret, app.Ticket)
		if err != nil {
			return
		}
		d.cache.Set(ctx, key, ct.ComponentAccessToken, time.Second*(time.Duration(ct.ExpiresIn-100)))
		return app.AppID, ct.ComponentAccessToken, nil
	}
	return d.conf.Open.Bytedance.AppId, componentAccessToken, nil
}

// GetBytedancePreAuthCode 创建预授权码
func (d *Dao) GetBytedancePreAuthCode(ctx context.Context) (*admin.GetBytedancePreAuthCodeReply, error) {
	componentAppID, componentAccessToken, err := d.GetBytedanceComponentAccessToken(ctx)
	if err != nil {
		return nil, err
	}
	var preAuthCode *bytedance.PreAuthCode
	preAuthCode, _, err = d.bytedance.ThirdParty.CreatePreAuthCode(
		ctx,
		componentAccessToken,
		componentAppID,
		&bytedance.CreatePreAuthCodeRequest{
			ShareRatio:  0,
			ShareAmount: 0,
		})
	if err != nil {
		return nil, err
	}
	return &admin.GetBytedancePreAuthCodeReply{
		PreAuthCode:    preAuthCode.PreAuthCode,
		ExpiresIn:      preAuthCode.ExpiresIn,
		ComponentAppId: componentAppID,
	}, nil
}

// AuthorizeBytedance 授权字节跳动
func (d *Dao) AuthorizeBytedance(ctx context.Context, authorizationCode string, expiresIn int64) (authAppID string, err error) {
	var componentAppID, componentAccessToken string
	if componentAppID, componentAccessToken, err = d.GetBytedanceComponentAccessToken(ctx); err != nil {
		return
	}

	var oauth *bytedance.OAuthToken
	oauth, _, err = d.bytedance.ThirdParty.GetOAuthToken(ctx,
		componentAppID,
		componentAccessToken,
		authorizationCode,
		"app_to_tp_authorization_code")
	if err != nil {
		return
	}
	now := time.Now()
	permissions, _ := json.Marshal(oauth.AuthorizePermission)
	var authApp *model.BytedanceOpenSubApp
	if authApp, err = d.LockBytedanceAuthApp(ctx, oauth.AuthorizerAppID); err != nil {
		// 前端比授权通知先到
		if errors.Is(err, gorm.ErrRecordNotFound) {
			authApp = &model.BytedanceOpenSubApp{
				AppID:                      oauth.AuthorizerAppID,
				TpAppID:                    componentAppID,
				AuthorizationCode:          authorizationCode,
				AuthorizationCodeExpiresAt: now.Add(time.Duration(expiresIn) * time.Second),
				AccessToken:                oauth.AuthorizerRefreshToken,
				RefreshToken:               oauth.AuthorizerRefreshToken,
				RefreshTokenExpiresAt:      now.Add(time.Duration(oauth.ExpiresIn) * time.Second),
				Status:                     types.BytedanceAuthorized,
				Permissions:                permissions,
			}
			if err = d.FromContext(ctx).Create(authApp).Error; err != nil {
				return
			}
			return authApp.AppID, err
		}
		return
	}
	authApp.AuthorizationCode = authorizationCode
	authApp.AccessToken = oauth.AuthorizerAccessToken
	authApp.Status = types.BytedanceAuthorized
	authApp.AccessTokenExpiresAt = now.Add(time.Duration(oauth.ExpiresIn-100) * time.Second)
	authApp.RefreshToken = oauth.AuthorizerRefreshToken
	authApp.RefreshTokenExpiresAt = now.Add(time.Hour*24*30 - 100*time.Second)
	authApp.Permissions = permissions
	if err = d.FromContext(ctx).Save(authApp).Error; err != nil {
		return
	}
	return authApp.AppID, err
}

// GetBytedanceSubApp 获取字节跳动子应用
func (d *Dao) GetBytedanceSubApp(ctx context.Context, appID string) (app *model.BytedanceOpenSubApp, err error) {
	app = new(model.BytedanceOpenSubApp)
	err = d.FromContext(ctx).Where("app_id = ?", appID).First(app).Error
	return
}

// SaveBytedanceMiniApp 保存字节跳动子应用
func (d *Dao) SaveBytedanceMiniApp(ctx context.Context, app *model.BytedanceMiniApp) error {
	return d.FromContext(ctx).Save(app).Error
}

// GetOrCreateBytedanceMiniApp 获取或者创建字节小程序应用信息
func (d *Dao) GetOrCreateBytedanceMiniApp(ctx context.Context, appID string) (app *model.BytedanceMiniApp, err error) {
	app = new(model.BytedanceMiniApp)
	err = d.FromContext(ctx).Where("app_id = ?", appID).FirstOrCreate(app).Error
	return
}

// GetBytedanceOpenApp 获取字节跳动开放app
func (d *Dao) GetBytedanceOpenApp(ctx context.Context) (app *model.BytedanceOpenApp, err error) {
	app = new(model.BytedanceOpenApp)
	err = d.FromContext(ctx).Where("app_id = ?", d.conf.Open.Bytedance.AppId).First(app).Error
	return
}

// UpdateBytedanceQRCode 更新小程序二维码
func (d *Dao) UpdateBytedanceQRCode(ctx context.Context, appID string) error {
	app := new(model.BytedanceMiniApp)
	err := d.FromContext(ctx).Where("app_id = ?", appID).First(app).Error
	if err != nil {
		return err
	}
	if app.QrCodeURL != "" {
		return nil
	}
	componentAppID, token, err := d.GetBytedanceAuthorizerAccessToken(ctx, appID)
	if err != nil {
		return err
	}
	resp, err := d.bytedance.MicroApp.DownloadQrcode(ctx,
		componentAppID,
		token,
		&bytedance.DownloadQrcodeRequest{
			Version: "current",
		})
	if err != nil {
		return fmt.Errorf("获小程序:%v(%v)二维码失败: %w", app.Name, app.AppID, err)
	}
	defer resp.Body.Close()
	key := fmt.Sprintf("ytedance-micro_app-%s.jpg", appID)
	putPolicy := storage.PutPolicy{
		Scope: fmt.Sprintf("image:%v", key),
	}
	mac := qbox.NewMac(d.conf.Qn.AccessKey, d.conf.Qn.SecretKey)
	upToken := putPolicy.UploadToken(mac)
	cfg := storage.Config{}
	cfg.Zone = &storage.ZoneHuadong
	cfg.UseHTTPS = true
	cfg.UseCdnDomains = false
	formUploader := storage.NewFormUploader(&cfg)
	ret := storage.PutRet{}
	buffer := make([]byte, resp.ContentLength)
	dataLen, _ := io.ReadFull(resp.Body, buffer)
	err = formUploader.Put(ctx, &ret, upToken, key, bytes.NewReader(buffer), int64(dataLen), nil)
	if err != nil {
		return fmt.Errorf("七牛上传失败")
	}
	app.QrCodeURL = d.conf.Qn.BucketUrl + "/" + ret.Key
	err = d.FromContext(ctx).Save(app).Error
	if err != nil {
		return err
	}
	return nil
}

// GetBytedanceMiniApp 获取字节跳动小程序信息
func (d *Dao) GetBytedanceMiniApp(ctx context.Context, appID string) (app *model.BytedanceMiniApp, err error) {
	app = new(model.BytedanceMiniApp)
	err = d.FromContext(ctx).Where("app_id = ?", appID).First(app).Error
	return
}

// GetBytedanceAppInfo 获取字节跳动小程序信息
func (d *Dao) GetBytedanceAppInfo(ctx context.Context, authAppID string) (appInfo *bytedance.AppInfo, err error) {
	var componentAppID, authorizerAccessToken string
	if componentAppID, authorizerAccessToken, err = d.GetBytedanceAuthorizerAccessToken(ctx, authAppID); err != nil {
		return
	}
	appInfo, _, err = d.bytedance.MicroApp.GetAppInfo(ctx, componentAppID, authorizerAccessToken)
	return
}

// GetLatestBytedanceTemplate 获取最新的微信模版
func (d *Dao) GetLatestBytedanceTemplate(ctx context.Context) (template *model.BytedanceTemplate, err error) {
	template = new(model.BytedanceTemplate)
	err = d.FromContext(ctx).Order("created_at desc").First(template).Error
	return
}

// SetBytedanceDomains 设置请求域名
func (d *Dao) SetBytedanceDomains(ctx context.Context, appID string) error {
	componentAppID, authorizerAccessToken, err := d.GetBytedanceAuthorizerAccessToken(ctx, appID)
	if err != nil {
		return err
	}
	_, _, err = d.bytedance.MicroApp.ModifyServerDomain(ctx,
		componentAppID,
		authorizerAccessToken,
		&bytedance.ModifyServerDomainRequest{
			Action:  "set",
			Request: d.conf.Open.Bytedance.Domains,
		})
	return err
}

// SetBytedanceWebViewDomains 设置业务域名
func (d *Dao) SetBytedanceWebViewDomains(ctx context.Context, appID string) error {
	componentAppID, authorizerAccessToken, err := d.GetBytedanceAuthorizerAccessToken(ctx, appID)
	if err != nil {
		return err
	}
	_, _, err = d.bytedance.MicroApp.ModifyWebviewDomain(ctx, componentAppID, authorizerAccessToken,
		&bytedance.ModifyWebviewDomainRequest{
			Action:  "set",
			Webview: d.conf.Open.Bytedance.WebViewDomains,
		})
	return err
}

// CommitBytedanceMini 提交字节跳动小程序代码
func (d *Dao) CommitBytedanceMini(ctx context.Context, appID string, req *bytedance.UploadPackageRequest) error {
	componentAppID, authorizerAccessToken, err := d.GetBytedanceAuthorizerAccessToken(ctx, appID)
	if err != nil {
		return err
	}
	if _, err = d.bytedance.MicroApp.UploadPackage(ctx, componentAppID, authorizerAccessToken, req); err != nil {
		return err
	}
	return nil
}

// CommitAuditBytedanceMini 提交字节跳动小程序审核
func (d *Dao) CommitAuditBytedanceMini(ctx context.Context, appID string) error {
	componentAppID, authorizerAccessToken, err := d.GetBytedanceAuthorizerAccessToken(ctx, appID)
	if err != nil {
		return err
	}
	if _, err := d.bytedance.MicroApp.CommitAuditPackage(
		ctx,
		componentAppID,
		authorizerAccessToken,
		&bytedance.CommitAuditPackageRequest{HostNames: []string{"douyin"}},
	); err != nil {
		return err
	}
	return nil
}

// ReleaseBytedanceMini 发布字节跳动小程序
func (d *Dao) ReleaseBytedanceMini(ctx context.Context, appID string) error {
	componentAppID, authorizerAccessToken, err := d.GetBytedanceAuthorizerAccessToken(ctx, appID)
	if err != nil {
		return err
	}
	if _, err = d.bytedance.MicroApp.ReleasePackage(ctx, componentAppID, authorizerAccessToken); err != nil {
		return err
	}
	return nil
}

// GetBytedanceMiniTestQRCode 获取字节跳动小程序体验码
func (d *Dao) GetBytedanceMiniTestQRCode(ctx context.Context, appID string, version string) (*http.Response, error) {
	componentAppID, authorizerAccessToken, err := d.GetBytedanceAuthorizerAccessToken(ctx, appID)
	if err != nil {
		return nil, err
	}
	resp, err := d.bytedance.MicroApp.DownloadQrcode(ctx,
		componentAppID,
		authorizerAccessToken,
		&bytedance.DownloadQrcodeRequest{
			Version: version,
		})
	if err != nil {
		return nil, err
	}
	return resp, nil
}

// CreateBytedanceMiniQRCode 创建字节跳动小程序二维码
func (d *Dao) CreateBytedanceMiniQRCode(ctx context.Context, appID string, req *v1.CreateBytedanceMiniQRCodeRequest) (*http.Response, error) {
	_, authorizerAccessToken, err := d.GetBytedanceAuthorizerAccessToken(ctx, appID)
	if err != nil {
		return nil, err
	}
	biz := &bytedance.CreateQRCodeRequest{
		AccessToken: authorizerAccessToken,
		AppName:     req.AppName,
		Path:        req.Path,
		Width:       req.Width,
	}
	if req.LineColor != nil {
		biz.LineColor = &bytedance.Color{
			R: req.LineColor.R,
			G: req.LineColor.G,
			B: req.LineColor.B,
		}
	}
	if req.Background != nil {
		biz.Background = &bytedance.Color{
			R: req.Background.R,
			G: req.Background.G,
			B: req.Background.B,
		}
	}
	resp, err := d.bytedance.MicroApp.CreateQRCode(ctx, biz)
	if err != nil {
		return nil, err
	}
	return resp, nil
}
